// ============================================================================
//  _ __   ___  ___  ___ _ __ ___  _   _
// | '_ \ / _ \/ __|/ _ \ '_ ` _ \| | | |
// | | | |  __/\__ \  __/ | | | | | |_| |
// |_| |_|\___||___/\___|_| |_| |_|\__,_|
//
// NES Emulator by daxnet, 2023
// MIT License
// ============================================================================

using NesEmu.Core;
using System.Collections.Concurrent;
using System.ComponentModel;

namespace NesEmu.Snake
{
    public partial class FrmMain : Form
    {
        #region Private Fields

        private const int TileSize = 12;

        private static readonly Brush[] _drawingBrushes = new Brush[]
        {
            new SolidBrush(Color.Black), new SolidBrush(Color.White), new SolidBrush(Color.Red), new SolidBrush(Color.Cyan),
            new SolidBrush(Color.Purple), new SolidBrush(Color.Green), new SolidBrush(Color.Blue), new SolidBrush(Color.Yellow),
            new SolidBrush(Color.Orange), new SolidBrush(Color.Brown), new SolidBrush(Color.OrangeRed), new SolidBrush(Color.DarkGray),
            new SolidBrush(Color.Gray), new SolidBrush(Color.LightGreen), new SolidBrush(Color.LightBlue), new SolidBrush(Color.LightGray)
        };

        private readonly Color _bitClearColor = Color.Red;
        private readonly Color _bitSetColor = Color.Green;
        private readonly Emulator _emulator = new();

        private readonly Graphics _graphics;
        private readonly ConcurrentQueue<string> _instructions = new();
        private readonly Random _rnd = new(DateTime.Now.Microsecond);
        private readonly byte[] _screenData = new byte[32 * 32];
        private string _currentInstruction = string.Empty;

        #endregion Private Fields

        #region Public Constructors

        public FrmMain()
        {
            InitializeComponent();
            _emulator.Cpu.Stepping += Cpu_Stepping;
            _emulator.Cpu.Stepped += Cpu_Stepped;
            _graphics = pictureBox1.CreateGraphics();
            DoubleBuffered = true;
        }

        #endregion Public Constructors

        #region Protected Methods

        protected override void OnClosed(EventArgs e)
        {
            _graphics.Dispose();
            _emulator.Cpu.Stepping -= Cpu_Stepping;
            _emulator.Cpu.Stepped -= Cpu_Stepped;
            foreach (var b in _drawingBrushes)
            {
                b.Dispose();
            }

            base.OnClosed(e);
        }

        #endregion Protected Methods

        #region Private Methods

        private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
        {
            BackgroundWorker worker = (BackgroundWorker)sender;
            while (!e.Cancel)
            {
                if (worker.CancellationPending)
                {
                    e.Cancel = true;
                    return;
                }

                _emulator.Cpu.Step();

                if (_instructions.Count >= 12)
                {
                    _instructions.TryDequeue(out _);
                }

                if (!string.IsNullOrEmpty(_currentInstruction))
                {
                    _instructions.Enqueue(_currentInstruction);
                }

                worker.ReportProgress(0, _emulator.Cpu);
            }
        }

        private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            if (e.UserState != null)
            {
                var cpuState = (Cpu)e.UserState;
                lblA.Text = $"${cpuState.A:x2}";
                lblX.Text = $"${cpuState.X:x2}";
                lblY.Text = $"${cpuState.Y:x2}";
                lblSP.Text = $"${cpuState.SP:x2}";
                lblPC.Text = $"${cpuState.PC:x4}";
                lblN.BackColor = cpuState.Flag.N ? _bitSetColor : _bitClearColor;
                lblV.BackColor = cpuState.Flag.V ? _bitSetColor : _bitClearColor;
                lbl5.BackColor = cpuState.Flag.Bit5 ? _bitSetColor : _bitClearColor;
                lbl4.BackColor = cpuState.Flag.Bit4 ? _bitSetColor : _bitClearColor;
                lblD.BackColor = cpuState.Flag.D ? _bitSetColor : _bitClearColor;
                lblI.BackColor = cpuState.Flag.I ? _bitSetColor : _bitClearColor;
                lblZ.BackColor = cpuState.Flag.Z ? _bitSetColor : _bitClearColor;
                lblC.BackColor = cpuState.Flag.C ? _bitSetColor : _bitClearColor;

                lblInstructions.Text = string.Join(Environment.NewLine, _instructions.ToArray());
            }
        }

        private void Cpu_Stepped(object? sender, CpuStepEventArgs e)
        {
            var update = false;
            for (ushort i = 0x200; i < 0x600; i++)
            {
                var colorByte = _emulator.Bus.ReadByte(i);
                if (_screenData[i - 0x200] != colorByte)
                {
                    _screenData[i - 0x200] = colorByte;
                    update = true;
                }
            }

            if (update)
            {
                UpdateScreen();
            }

            if (e.IsBreakInstruction)
            {
                backgroundWorker1.CancelAsync();
                return;
            }
        }

        private void Cpu_Stepping(object? sender, CpuStepEventArgs e)
        {
            _emulator.Bus.WriteByte(0xfe, (byte)_rnd.Next(0, 128));
            _currentInstruction = e.DisassembledInstruction;
        }

        private void FrmMain_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.KeyCode == Keys.Escape)
            {
                backgroundWorker1.CancelAsync();
            }
            else
            {
                var key = e.KeyCode;
                switch (key)
                {
                    case Keys.W:
                        _emulator.Bus.WriteByte(0xff, 0x77);
                        break;

                    case Keys.S:
                        _emulator.Bus.WriteByte(0xff, 0x73);
                        break;

                    case Keys.A:
                        _emulator.Bus.WriteByte(0xff, 0x61);
                        break;

                    case Keys.D:
                        _emulator.Bus.WriteByte(0xff, 0x64);
                        break;
                }
            }
        }

        private void mnuDisassemble_Click(object sender, EventArgs e)
        {
            if (openFileDialog1.ShowDialog(this) == DialogResult.OK)
            {
                _emulator.InstallCartridge(new Cartridge(openFileDialog1.FileName));
                var asm = _emulator.Disassemble();

                var disamForm = new FrmDisassembler(asm);
                disamForm.Show(this);
            }
        }

        private void mnuOpenRom_Click(object sender, EventArgs e)
        {
            if (openFileDialog1.ShowDialog() == DialogResult.OK)
            {
                _emulator.Open(openFileDialog1.FileName);
                backgroundWorker1.RunWorkerAsync();
            }
        }

        private void startToolStripMenuItem_Click(object sender, EventArgs e)
        {
            _emulator.Open("snake2.nes");
            backgroundWorker1.RunWorkerAsync();
        }

        private void UpdateScreen()
        {
            if (_screenData != null)
            {
                for (ushort i = 0; i < _screenData.Length; i++)
                {
                    var cb = _screenData[i];
                    var x = i % 32;
                    var y = i / 32;
                    _graphics.FillRectangle(_drawingBrushes[cb % 0x10], x * TileSize, y * TileSize, TileSize, TileSize);
                }
            }
        }

        #endregion Private Methods
    }
}